// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2023. All rights reserved.
 * Author: Huawei OS Kernel Lab
 * Create: Fri Sep 23 10:45:12 2022
 */

#include <linux/file.h>
#include <linux/slab.h>
#include <linux/mount.h>
#include <linux/mutex.h>
#include <liblinux/kernfs.h>
#include <linux/path.h>
#include <trace/hooks/liblinux.h>

#include "../../fs/proc/internal.h"

#define PROC_SYS_PREFIX "sys"

static struct vfsmount *proc_sys_mnt;
static DEFINE_MUTEX(path_lock);

struct liblinux_proc_sys_entry {
	char *name;
	int namelen;
	struct liblinux_proc_sys_entry *parent;
};

static int __liblinux_proc_register(const char *path, mode_t mode,
			 struct liblinux_pal_kernfs_ops *ops, void *data)
{
	int ret;
	void *ptr_ret;

	if (!proc_sys_mnt) {
		struct file_system_type *proc_fs_type;

		proc_fs_type = get_fs_type("proc");
		if (!proc_fs_type) {
			pr_err("Failed to find procfs\n");
			return -EINVAL;
		}

		ptr_ret = kern_mount(proc_fs_type);
		put_filesystem(proc_fs_type);
		if (IS_ERR(ptr_ret)) {
			pr_err("Failed to mount procfs\n");
			return (int)PTR_ERR(ptr_ret);
		}
		proc_sys_mnt = ptr_ret;
	}

	if (S_ISREG(mode)) {
		ret = liblinux_pal_procfs_create_file(path, ops, data, (unsigned int)mode);
	} else if (S_ISDIR(mode)) {
		ret = liblinux_pal_procfs_create_dir(path, (unsigned int)mode);
	} else {
		pr_warn("liblinux: unsupported proc sys entry '%s', mode=0x%x\n", path, mode);
		ret = 0;
	}

	if ((ret < 0) && (ret != -EEXIST))
		pr_err("liblinux: create proc ct '%s' failed, mode=0x%x, ret=%d\n",
		       path, mode, ret);

	return ret;
}

static char *
__liblinux_mk_proc_sys_path(struct ctl_dir *dir, const char *name,
			    int nlen, char *proc_sys_path, int plen)
{
	char *p = &proc_sys_path[plen - 1];
	int prefix_len = strlen(PROC_SYS_PREFIX);
	int len = 0;

	if (name == NULL || (nlen + prefix_len) >= plen - 1)
		return NULL;

	*--p = '\0';

	/* proc sys table */
	p -= nlen;
	memcpy(p, name, nlen);
	*--p = '/';

	/* proc sys dir */
	do {
		const char *procname = dir->header.ctl_table[0].procname;

		if (procname == NULL || strlen(procname) == 0)
			break;

		len = strlen(procname);
		if (p - proc_sys_path < len + prefix_len + 1) {
			proc_sys_path[0] = '\0';
			pr_err("proc sys path is too long: %s/.../%s/%s\n",
			       PROC_SYS_PREFIX, procname, p);
			p = NULL;
			break;
		}

		p -= len;
		memcpy(p, procname, len);
		*--p = '/';
		dir = dir->header.parent;
	} while (dir != NULL);

	/* proc sys prefix */
	if (p != NULL) {
		p -= prefix_len;
		memcpy(p, PROC_SYS_PREFIX, prefix_len);
	}

	return p;
}

static char *
__liblinux_acquire_proc_sys_path(struct rb_node *node, char *buf, int len)
{
	struct ctl_node *cnode = NULL;
	struct ctl_table *ct = NULL;
	struct ctl_table_header *head = NULL;

	cnode = rb_entry(node, struct ctl_node, node);
	head = cnode->header;
	ct = &head->ctl_table[cnode - (head)->node];

	return __liblinux_mk_proc_sys_path(head->parent, ct->procname,
					   strlen(ct->procname), buf, len);
}

static int __liblinux_proc_sys_rw(unsigned long long pos, unsigned long ubuf,
		       unsigned long size, void *ctx,
		       unsigned long *rw_size /* out */, bool do_write)
{
	char *buf = NULL;
	char *pathname = NULL;
	struct file *filep = NULL;
	int ret;
	loff_t pos_loff;
	const struct path root = {.mnt = proc_sys_mnt, .dentry = proc_sys_mnt->mnt_root};

	if ((ctx == NULL) || (pos > (unsigned long long)LLONG_MAX))
		return -EINVAL;

	pos_loff = (loff_t)pos;
	buf = kmalloc(PATH_MAX, GFP_KERNEL);
	if (buf == NULL)
		return -ENOMEM;

	pathname = __liblinux_acquire_proc_sys_path((struct rb_node *)ctx, buf, PATH_MAX);
	if (pathname == NULL) {
		ret = -ENAMETOOLONG;
		goto err;
	}

	filep = file_open_root(&root,
			       pathname, (do_write ? O_WRONLY : O_RDONLY), 0);
	if (IS_ERR(filep)) {
		ret = (int)PTR_ERR(filep);
		goto err;
	}

	if (do_write)
		ret = vfs_write_full(filep, (const char __user *)ubuf, size, &pos_loff);
	else
		ret = vfs_read_full(filep, (char __user *)ubuf, size, &pos_loff);

	if (ret >= 0) {
		*rw_size = ret;
		ret = 0;
	}

	filp_close(filep, NULL);
err:
	kfree(buf);
	return ret;
}

static int __liblinux_proc_sys_read(unsigned long long pos, unsigned long ubuf,
			 unsigned long size, void *ctx,
			 unsigned long *rsize /* out */)
{
	return __liblinux_proc_sys_rw(pos, ubuf, size, ctx, rsize, false);
}

static int __liblinux_proc_sys_write(unsigned long long pos, unsigned long ubuf,
			  unsigned long size, void *ctx,
			  unsigned long *wsize /* out */)
{
	return __liblinux_proc_sys_rw(pos, ubuf, size, ctx, wsize, true);
}

static struct liblinux_pal_kernfs_ops liblinux_proc_sys_ops = {
	.read = __liblinux_proc_sys_read,
	.write = __liblinux_proc_sys_write,
};

static int __procfs_sys_mk_path(struct liblinux_proc_sys_entry *entry, const char *name, int pos)
{
	int ret, len;

	if (name == NULL)
		return 0;

	len = strlen(name);
	if ((pos + len) >= PATH_MAX)
		return -ENAMETOOLONG;

	ret = snprintf((entry->name + pos), (PATH_MAX - pos), "/%s", name);
	if (ret > (len + 1) || ret < 0)
		return -EINVAL;

	/* hm-fs handle the end of path is not '/' only */
	entry->namelen = pos + ((name[len - 1] == '/') ? (ret - 1) : ret);
	entry->name[entry->namelen] = '\0';

	return 0;
}

static int __procfs_sys_register(struct liblinux_proc_sys_entry *entry,
		      struct ctl_table_header *head,
		      struct ctl_table *ct)
{
	int ret;
	int dirlen = 0;
	struct rb_node *node = NULL;
	const char *name = NULL;
	struct ctl_table *table = NULL;

	if (entry == NULL || ct == NULL)
		return -EINVAL;

	table = ct;
	dirlen = entry->namelen;
	for (; (name = table->procname) != NULL; table++) {
		ret = __procfs_sys_mk_path(entry, name, dirlen);
		if (ret < 0)
			return ret;

		node = &head->node[table - head->ctl_table].node;
		if (table->child != NULL) {
			ret = __liblinux_proc_register(entry->name, (umode_t)S_IFDIR,
						       NULL, NULL);
			if ((ret < 0) && (ret != -EEXIST))
				return ret;

			ret = __procfs_sys_register(entry, head, table->child);
		} else {
			ret = __liblinux_proc_register(entry->name, (umode_t)S_IFREG | table->mode,
						       &liblinux_proc_sys_ops, (void *)node);
		}
	}

	return ret;
}

int liblinux_procfs_sys_register(const char *dir, struct ctl_table_header *head,
			     struct ctl_table *table)
{
	static char proc_sys_path[PATH_MAX] = PROC_SYS_PREFIX;
	struct liblinux_proc_sys_entry entry = {
		.name = proc_sys_path,
		.namelen = strlen(PROC_SYS_PREFIX),
		.parent = NULL,
	};
	int ret;

	if ((table == NULL) || (head == NULL))
		return -EINVAL;

	mutex_lock(&path_lock);
	ret = __procfs_sys_mk_path(&entry, dir, entry.namelen);
	if (ret < 0)
		goto err;

	/* make sure dir exist */
	ret = __liblinux_proc_register(entry.name, (umode_t)S_IFDIR, NULL, NULL);
	if ((ret < 0) && (ret != -EEXIST))
		goto err;

	ret = __procfs_sys_register(&entry, head, table);
err:
	mutex_unlock(&path_lock);
	return ret;
}

static void hook_register_sysctl_table(void *__data, const char *dir,
				       struct ctl_table_header *head, struct ctl_table *table)
{
	liblinux_procfs_sys_register(dir, head, table);
}

INIT_VENDOR_HOOK(ldk_vh_register_sysctl_table, hook_register_sysctl_table);

void liblinux_procfs_sys_unregister(char *name)
{
	pr_err("procfs sys unregister not support now!!\n");
}
